""" sbak.entity - Defines the entity systems and basic entities.

    TODO: Classify entity management; remove dependency on mod. funcs.
"""

import weakref
import pygame
from pygame import Rect

from sbak import ID_ANY
from sbak import debug
from sbak.error import FormatError

__all__ = (
    'Entity',
    'Actor',
    'Camera',
    'Background',
    'TileMap'
    
    'init',
    'add_type',
    'get',
    'register',
    'create',
    'from_json',
    'generate_id',
)

_init = False # Whether or not this module is initialized.

def init():
    """ Initializes the entity module.
    
        This should be called before using anything out of the entity
        package or else an error might occur when an entity is created.
    """
    global _init, _typefactory, _typejson, _entity, _ids
    if not _init:
        # All registered entity types (maps tag -> factory function)
        _typefactory = {}
        
        # All registered entity tags mapped to JSON deserializers
        _typejson = {}
        
        # All IDs used since initialization. This should NEVER contain the
        # special value of ID_ANY as this vlue is reserved.
        _ids = set()
        
        # All entities in existence
        _entity = weakref.WeakValueDictionary()
        
        # Register the default entity tags.
        add_type('entity', Entity)
        add_type('actor', Actor)
        add_type('camera', Camera)
        add_type('background', Background)
        
        _init = True

def add_type(tag, factory, json=None):
    """ Registers an entity-like object factory with the given tag.
        
        tag:        The string to associate as the tag for the entity
                    type.
                    
        factory:    A callable that will generate entities of a certian
                    tag. The only requirement for this callable is
                    that it must accept at least one argument and the
                    first argument must be called 'id'.
                    
        json:       A callable that will be given a JSON-compatible
                    object. If not present then the factory above
                    will be used with the json values passed as
                    keyword arguments.
        
        The object returned by either factory should conform to the
        basic entity standard (see the base.Entity class).
        
        Raises TypeError if tag is not a string.
    """
    if not isinstance(tag,str):
        raise TypeError("value of 'tag' must be string, got %s"%tag)
    _typefactory[tag] = factory
    _typejson[tag] = json

def get(id):
    """ Returns the first entity with the given ID.
        If no entity exists with the ID, a ValueError is raised.
    """
    try:
        return _entity[id]
    except KeyError:
        raise ValueError("No entity with ID '%s'" % id)

def register(entity):
    """ Registers an entity object.
    
        You don't normally need to call this on an entity because the
        constructor for the base entity will register the entity
        itself.
        
        Note that an entity MUST have an ID in order to be regsitered.
        If it does not have an id, a ValueError will occur.
    
        If the entity's ID is already in use by another registered
        entity, ValueError will be raised.
        
        Note that as soon as soon as an entity is destroyed it will
        automatically be "unregistered" through weak referencing.
    """
    if not hasattr(entity, 'id'):
        raise ValueError("Entity must have ID to be registered.")
    
    if entity.id in _entity:
        raise ValueError("Cannot register entity; ID '%s'already in use."%entity.id)
    
    _ids.add(entity.id)
    
    _entity[entity.id] = entity

def create(tag=None, id=None, *vargs, **kwargs):
    """ Creates a new object using an entity factory.
        
        tag:    The tag for the entity. This string must
                be registered with an entity factory. If None is
                passed, "entity" is the assumed value.
                
        id:     An ID for the new entity. It should not be in use by an
                existing entity.
        
        You may pass a series of variable or keyword arguments if you
        wish. These will be passed on to the entity factory. It is up
        to the factory to decide what to do with them, if anything.
        
        Returns an object of the type returned by the factory
        corresponding to the given tag.
    """
    if tag is None:
        tag = 'entity'
        
    ent = _typefactory[tag](id,*vargs,**kwargs)
    
    return ent

def from_json(json, resman=None):
    """ Create a new object from a JSON object (dict) using a factory.
    
        json:   The JSON object to use.
        
        resman: A resource manager to use for interpreting resource
                IDs.
    
        The JSON object passed must have at least the following data:
        
        tag:    A tag associated with a JSON object deserializer/factory.
                The object passed to from_json
                will be passed on to this callable and the object
                it returns will be returned by from_json.
    """
    try:
        tag = json['tag']
    except KeyError:
        raise ValueError("JSON object must have a 'tag'.")
    
    factory = _typejson[tag]
    
    if factory is not None:
        ent = factory(json,resman)
        
    else:
        json = dict(json)
        if resman is not None:
            json['image'] = resman.get(json['image'])
        if 'world' in json:
            del json['world']
        ent = create(**json)
    
    return ent

def generate_id(tag):
    """ Generates an unused entity ID string based on a tag.
    
        tag:    Give an entity tag string or an object with a .tag
                attribute. If the .tag attribute exists, that value
                will be used, else the given value. Case sensitive.
                
        This will return a string of the form "tagIDN" where 'tag' is
        the given value (or its .tag attribute) and 'IDN' is an integer
        that is at least 0.
        
        The system internally keeps track of ID strings which have been
        used previously in order to make sure that the same ID is never
        generated twice. On that note, this function will never return
        the same value more than once. However, values are chosen
        sequentially rather than randomly, so it can be expected that
        the IDN portion of the string will always be at least 1 greater
        than the previously returned value, provided that the given
        string is the same.
        
        Note that the special value "%s" (the value of ID_ANY) is not
        allowed as the given tag as that is a reserved value.
        Giving this as a value will raise ValueError.
        
        Raises TypeError if tag (or tag.tag) is not a string.
    """
    global _ids
    
    # If the object is a class or class instance with a .tag attribute,
    # use that instead.
    if hasattr(tag,'tag'):
        tag = tag.tag
    
    # Only allow strings to be used.
    if not isinstance(tag,basestring):
        raise TypeError("tag (or tag.tag) must be a string, not %s."%
                        tag.__class__.__name__)
    
    # Don't allow ID_ANY.
    if tag==ID_ANY:
        raise ValueError("Special id \"%s\" is not an allowed tag."%tag)
    
    # Search for an unused ID
    num = 0
    newid = "%s%i" % (tag, num)
    while newid in _ids:
        num += 1
        newid = "%s%i" % (tag, num)
    
    # Add the new ID to the used ID set.
    _ids.add(newid)
    
    return newid

generate_id.__doc__ = generate_id.__doc__%ID_ANY


from base import Entity, Actor
from common import Background, Camera
from tilemap import TileMap